聊聊 React 编程思想

前阵子在部门内做了一个关于 React 的分享,为了更进一步地理解,也分享给有需要的小伙伴,做了如下整理 ^_^

image

Facebook 在 2013 年 5 月推出了全新的函数式编程,也就是全球范围内使用人数最多的前端框架 - React,也是目前最受欢迎的前端框架之一

现代框架与旧式框架的区别

React 是一个视图层框架,是用来解决数据和页面渲染的问题

同样的还要 VueAngular,这也是目前前端最受欢迎的三套框架,几乎是所有前端工程师必备的一项技能

所以我们在技术选型的时候,常常会产生困扰,究竟应该选择哪一门语言开发项目呢,比如

  • reactjs : 灵活性更大,处理大型业务时选择性更多一点
  • vuejs : api更多,实现功能更简单,但也因为api多,灵活性有一定限制

所以,在做复杂度比较高的项目时,大家倾向于 reactjs ,而面向用户端的一些复杂度不是特别高的项目时,用 vuejs 更简单

当然,vuejs 也可以做大型的项目,至于具体选什么框架,还需要取决于对框架的熟悉程度以及业务复杂度做一个权衡

而在几年前开发前端应用时,基本没这个困扰,因为大家开发都用 jQuery

image

但随着前端交互的复杂度越来越高,现代框架比如 reactvue 逐渐的替换掉了 jQuery,因为用现代框架来开发更容易维护

为什么会说变得容易维护呢?我们先来看看 reactjQuery 到底什么区别

image

我认为,他们之间编程思想的最大区别,就是声明式命令式的区别

命令式

命令式编程,比如 jQuery,直接操作 DOM,告诉页面怎么挂载,怎么操作,整个程序有 70% 都是操作 DOM

<button class="red_btn" type="button">change</button> 
<script type="text/javascript"> 
  $(document).ready(function(){ 
    $("button").click(function(){ 
      if($("button").hasClass("red_btn")){ 
        $("button").removeClass("red_btn").addClass("green_btn") 
      } else {
        $("button").removeClass("green_btn").addClass("red_btn")
      }
    }) 
  })
</script>

例如上面的代码,点击一个按钮,切换 button 颜色。我们用 jQuery 这种命令式编程思路写,就是当前是什么颜色就让它变成另外一个颜色

但如果我们认真想想,其实这里面可以细分成两个行为,一个是对状态判断,另一个是操作 DOM。那声明式呢?

声明式

还是上面那个场景,我们用 React 提供的 JSX 语法来实现,当我们用 JSX 描述了映射关系之后,点击按钮事件时,只需要对颜色这个变量进行修改就可以完成需求了

handleChangeBtnColor() {
  this.setState({
    tag: !this.state.tag
  })
}

render() {
  return (
    <div>
      <button
        className={this.state.tag? 'red_btn' : 'green_btn'}
        type="button"
        onClick={this.handleChangeBtnColor}
      >
        change
      </button>
    </div>
  )
}

所以区别就出来了,用 react 来实现同样的需求,如果细分来看,我们在逻辑上只有状态这一个行为

jQuery 是两个行为,状态 + DOM 操作

那为什么 React 要用声明式来编程呢?

因为命令式编程是直接操作 DOM 节点,如果有多个事件,比如 100 个 button 上都有点击和取消事件,那频繁的事件操作会很大程度上产生 bug ,而且不可控,会有无法预期性的 bug 产生

而且声名式编程只需要我们操作数据就好,数据可能出现的几种情况我们能提前做好容错

声明式是通过描述状态与视图之间的映射关系来操作DOM,或者说具体点是用这样的映射关系来生成一个 DOM 节点插入到页面去

比如 React 提供的 JSX 和 Vue 中的模板语言

目的是为了实现声明式渲染的功能,本质上都是描述了 『状态』与『视图』之间的映射关系

状态与视图之间的映射关系,等同于 render 函数

在框架的内部,不论是 JSX 还是 Vue 的模板,最终会编译成 render 函数。

image

声明式渲染是现代框架的特性,也就是我们常说的数据驱动视图

这个特性跟声明式可以简化维护应用代码的复杂度有什么关系呢?

事实上,这个特性可以让我们把关注点只放在状态的维护上。这样一来,即使应用复杂后,我们管理代码的方式只在状态上,所有的视图操作都不用关心了,因为框架会帮我们自动去做,可以说大大降低代码维护的成本

what

状态

那我们所说的这个状态到底是什么呢?

  • 在现实中,状态是某一时刻看到或感受到的状况
  • 对于设计来说,状态是 UI 在交互过程中某一时刻的画面
  • 对于开发来说,状态是存储上下文中所用到的数据

image

React 官网上,我们可以看到其介绍是用于构建用户界面的 JavaScript,所以 React 本质上是一个创建 UI 接口的视图层框架

前面我们已经提到了 React 有个声明式思想,它是数据到视图的一个静态映射,但在我们的实际项目中,并不是一个静态网页,还需要操作数据,网页的状态可能随时改变,那怎么才能让网页跟着状态一起改变呢?

响应式设计思想

这就是 React 背后的响应式设计思想

开发者只需要告诉 React 我们希望页面长什么样子,React 就会自动帮我们绘制界面

也就是说,我们只要操作数据,页面视图会自动作出响应,用户界面的展示完全取决于数据层

而且我们一切的操作都是基于内存之中,不会有较大的性能损耗,这就是 React 响应式编程的精髓,也是为何它叫作 ReactReact 在英文中是响应的意思

简单来说就是,不需要关注视图层,只需要关注数据层的变化

一个类 class 一定有一个构造函数 constructor,最优先被执行

constructor(props) {
  super(props)
}

constructor 接收 props 参数,super 指的是父类 Componentsuper(props) 方法指的是调用父类的构造函数

定义数据需要定义在状态里面 this.state

框架是怎么知道 Web 应用在运行时数据状态发生了变化呢? 这个问题是所有框架必须去解决的

不同的解决方案,导致的直接结果就是它所提供给用户的上层语法或 API 完全不一样,也是我们常对比的各个框架的使用区别

解决方案包括我们常说的 Virtual DOMdiff 算法对比

Virtual DOM 有兴趣的小伙伴可以查看我的另一篇博客 Virtual DOM 中那些你不知道的事,在这篇博客里有对 Virtual DOM 做一个详细的讲解

服务端渲染

既然提到了 Virtual DOM,这里就提一下 React 的价值 - nodejs 服务端渲染

因为有 Virtual DOM 的存在,React 可以很容易的将 Virtual DOM 转换为字符串,这便使我们可以只写一份 UI 代码,同时运行在 node 里和和浏览器里

var html = React.renderToString(el)

在 node 里将组件 HTML 渲染为一段 HTML 一句话即可,不过围绕 renderToString 还需要做一些准备工作

整个思路大致是:

  1. 从后台 server 或数据库等来源拉取数据
  2. 引入要渲染的 React 组件
  3. 调用 React.renderToString() 方法来生成 HTML
  4. 最后发送 HTML 和数据给浏览器

这就是 React 的服务端渲染,组件的代码前后端都可以复用

不仅如此,React 还能够用一套代码同时运行在浏览器和 node 里,而且能够以原生 App 的姿势运行在 iOS 和 Android 系统中,即拥有了 web 迭代迅速的特性,又拥有原生 App 的体验,也就是 React-Native

单向数据流

我认为使用 react最大好处在于功能组件化,遵守前端可维护的原则

react 是单向数据流,什么是单向数据流呢?

  • 数据主要从父节点传递到子节点( 通过 props ),即遵循从上到下的数据流向

  • 如果顶层( 父级 )的某个 props 改变了,react 会重渲染所有的子节点

image

通俗的理解是指用户访问 ViewView 发出用户交互的 Action,在 Action 里对 state 进行相应更新。state 更新后会触发 View 更新页面的过程。这样数据总是清晰的单向进行流动,便于维护并且可以预测

那为什么 react 要使用单向数据流呢?

实际上,单向数据流这种模式十分适合跟 react 搭配使用

它的主要思想组件不会改变接收的数据。它们只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值。当组件的更新机制触发后,它们只是使用新值进行重新渲染而已

消除了在多个地方同时管理状态,可能出现的数据不同步的情况,它只会在一个地方进行状态管理,减小了应用的复杂度,唯一的数据源将使得开发更加简单

需要注意的是,单向数据流并非单向绑定,甚至单向数据流与绑定没有任何关系

对于 react 来说,单向数据流( 从上到下 )与单一数据源这两个原则,限定了 react 中要想在一个组件中更新另一个组件的状态(类似于 vue 的平行组件传参,或者是子组件向父组件传递参数),需要进行状态提升

即将状态提升到他们最近的祖先组件中。子组件中 Change 了状态,触发父组件状态的变更,父组件状态的变更,影响到了另一个组件的显示(因为传递给另一个组件的状态变化了,这一点与 vue 子组件的 $emit() 方法很相似)

比如在做 list 删除时,为什么不可以直接把 list 传给子组件来改变 list?

因为父组件可以向子组件传值,但是子组件只能去使用这个值,不能去改变这个值

应该是父组件向子组件传递方法,子组件调用这个方法,传递一个数据,最终还是父组件自己来改变这个数据

Vue也是单向数据流,只不过能实现双向绑定,UI 控件提供了双向数据绑定的方式,在一些需要实时反应用户输入的场合会非常方便

但通常认为复杂应用中这种便利比不上引入状态管理带来的优势

所以无论是 vue 还是 react 其实还是提倡单向数据流去管理状态,这一点在 vuexredux 状态管理器上体现的很明显

虽然 vuereact 框架本身有自己状态管理,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态

所以就需要 vuexredux 来解决这个问题,redux 在我的另一篇博客大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux 中介绍的很详细了,大家有兴趣可以去看看

注意:

单向数据流中的单向,指的是数据从父组件到子组件的这个流向叫单向

绑定单双向是指View层与Module层之间的映射关系

但我们通常也说双向数据绑定,带来双向数据流

数据( state )和视图( View )之间的双向绑定,ng 里的 ng-model 和 vue 里的 v-model

props

刚才我们提到了 props,怎么理解 props 呢?

propsproperty 的缩写,可以理解为 HTML 标签的 attribute

在组件内部,可以通过 this.props 来访问 propsprops 是组件唯一的数据来源

不可以使用 this.props 直接修改 props,因为 props 是只读的props 是用于整个组件树中传递数据和配置

PropTypes 与 DefaultProps

react 为我们提供了一套非常简单好用的属性校验机制,强校验:

// 对TodoItem的一些属性类型做校验
TodoItem.propTypes = {
  content: PropTypes.string.isRequired,
  deleteItem: PropTypes.func,
  index: PropTypes.number
}

// 设置TodoItem的一些默认属性
TodoItem.defaultProps = {
  content: 'hello world'
}

PropTypes 包含的校验类型包括基本类型、数组、对象、实例、枚举

state

React 的一大创新,就是把每一个组件都看成是一个状态机,组件内部通过 state 来维护组件状态的变化,这也是state 唯一的作用

每个组件都有属于自己的 statestateprops区别在于前者 ( state ) 只存在于组件内部,只能从当前组件调用 this.setState 修改 state 值( 不可以直接修改 this.state

一般我们更新子组件都是通过改变 state 值,更新子组件的 props 值从而达到更新

state 一般和事件一起使用,比如有一个简单的开关组件,开关状态会以文字的形式表现在按钮的文本上

首先需要在 render 方法中返回了一个 button 元素,给 button 注册了一个事件用来处理点击事件,在点击事件中对 state 的描述开关状态的字段,比如 on 取反,并执行 this.setState() 方法设置 on 字段的新值。一个开关组件就完成了

react 通过将事件处理器绑定到组件上来处理事件

react 事件本质上和原生 JS 一样,鼠标事件用来处理点击操作,表单事件用于表单元素变化等,react 事件的命名、行为和原生 JS 差不多,不一样的地方是 react 事件名区分大小写

事件的处理器需要由组件的使用者来提供,可以通过 props 将事件处理器传进来

image

这是一个 react 组件实现组件可交互所需的流程,render() 输出 Virtual DOMVirtual DOM 转为 DOM,再在 DOM 上注册事件,事件触发 setState() 修改数据,在每次调用 setState 方法时,react 会自动执行 render 方法来更新 Virtual DOM,如果组件已经被渲染,那么还会更新到 DOM 中去

setState 方法

新版的 setState 可以接收一个函数而不是一个对象了,需要有一个返回值 return
所以我们可以在项目中做一些优化,比如

handleInputChange(e) {
  this.setState({
    inputValue: e.target.value
  })
}

可以优化为

handleInputChange(e) {
  const value = e.target.value
  this.setState(() => ({
      inputValue: value
  }))
}

注意:当有 e.target.value 这种异步设置数据的时候,需要存在外层

handleBtnClick() {
  this.setState({
    list: [...this.state.list, this.state.inputValue],
    inputValue: ''
  })
  }

可以用 prevState,改为

handleBtnClick() {
  this.setState((prevState) =>({
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  }))
}

handleDeleteItm(index) {
  const list = [...this.state.list] // 拷贝list数组
  list.splice(index,1)
  this.setState({
    list
  })
}

可以改为

handleDeleteItm(index) {
  this.setState((prevState) => {
    const list = [...prevState.list]
    list.splice(index,1)
    return {list}
  })
}

props 与 state

尽可能使用 props 当做数据源,state 用来存放状态值( 简单的数据 )

也就是说咱们通常用 props 传递大量数据,state 用于存放组件内部一些简单的定义数据

当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行

当父组件的 render 函数被运行时,它的子组件的 render 都将重新被运行一次

单向数据流和单向数据绑定是什么区别呢

前面已经提到了单向数据流,需要按照它的顺序办事。比如我们假设有一个这样的生命周期:

  1. 从 data 里面读取数据
  2. ui 行为( 如果没有 ui 行为就停在这里等他有了为止 )
  3. 触发 data 更新
  4. 再回到步骤1

改了一个数,view 层不能反回头来找他来更新 view 层视图( 从步骤 2 跳回去 1 ),你得等下一个循环(转了一圈)的步骤 1 才能更新视图。react 就是这样子,你得 setState 触发更新,如果你 this.state = {...},是没用的,它一直不变

单向数据绑定,就是绑定事件,比如绑定 onInputonChangestorage 这些事件,只要触发事件,立刻执行对应的函数(代表 react)

双向数据绑定,我们一般是借用 js 底层的 Object.defineproperty ( 代表 Vue )

这是 Vue 双绑的核心思想,view 层能让 model 层变了,model 层也能让 view 层变了

要判断是单向绑定还是双向绑定,只需要手动去控制台改一下那个核心绑定的数据,view 层的显示内容能马上变化的就是双绑,不能马上有变化的只是单向数据

想做到像 Vue 那样的极致双绑,能够在控制台改个数据就改变视图的,大概就只有 defineproperty(据说新版 vue 现在用 ES6proxy )和定时器轮询了

既然说到了数据流,那组件间是怎么进行通信的呢?

组件通信

一般来说,有两种通信方式

父子组件通信

react 中,最为常见的组件通信也就是父子了,一般情况是:

父组件更新组件状态 -----props-----> 子组件更新

另一种情况是

子组件更新父组件状态 -----需要父组件传递回调函数-----> 子组件调用触发

可能大家对于第二种子组件更新父组件状态的情况有些不理解

一般情况下,只能由父组件通过 props 传递数据给子组件,使得子组件得到更新

那么现在,我们想实现子组件更新父组件,就需要父组件通过 props 传递一个回调函数到子组件中,这个回调函数可以更新父组件子组件就是通过触发这个回调函数,从而使父组件得到更新

兄弟组件通信

当两个组件处于同一级时( 同处父级,或者同处子级 ),就称为兄弟组件

这里也有两种实现方式

方式一

按照React单向数据流方式,我们需要借助父组件进行传递,通过父组件回调函数改变兄弟组件的props

其实这种实现方式与子组件更新父组件状态的方式是大同小异的

方式一只适用于组件层次很少的情况,当组件层次很深的时候,整个沟通的效率就会变得很低

方式二

React官方给我们提供了一种上下文方式,可以让子组件直接访问祖先的数据或函数,无需从祖先组件一层层地传递数据到子组件中

但这种方法建议按需使用,可能会导致一些不可预期的错误。( 比如数据传递逻辑结构不清晰 )

组件划分

前面已经提到使用 react最大好处在于功能组件化,遵守前端可维护的原则

事实上,react 组件化开发原则是组件负责渲染 UI,组件的不同状态对应着不同 UI,通常遵循以下组件设计思路

  • 布局组件:仅仅涉及应用 UI 界面结构的组件,不涉及任何业务逻辑,数据请求及操作

  • 容器组件:负责获取数据,处理业务逻辑,通常在 render() 函数内返回展示型组件

  • 展示型组件:负责应用的界面 UI 展示

  • UI 组件:指抽象出的可重用的 UI 独立组件,通常是无状态组件

实际项目中,最好将 UI 组件和容器组件拆分, UI 组件负责页面渲染,容器组件负责页面逻辑

当组件中只有一个 render 函数时,就可以定义成无状态组件

无状态组件的性能比较高,因为它就是一个函数,而 React 里边普通的组件是 JS 里边的一个类,这个类生成的对象里,还会有一些生命周期函数,所以它执行起来,既要执行生命周期函数,又要执行 render ,它要执行的东西远比函数执行的东西多的多,所以一个普通组件的性能是肯定赶不上无状态组件的

生命周期函数

React的组件拥有一套清晰完整而且非常容易理解的生命周期机制

大体可以分为三个过程:初始化更新销毁

在组件生命周期中,随着组件的 props 或者 state 发生改变,它的 Virtual DOMDOM 表现也将有相应的变化

6721543734532_ pic

什么叫生命周期函数?生命周期函数指在某一时刻组件会自动调用执行的函数

// constructor 可以理解为一个生命周期函数,它是 ES6 的语法规定的。在组件一创建就会被调用,页面初始化 Initialization
constructor(props) {
  super(props)
  // 当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行  
  this.state = {
    inputValue: 'hello',
    list: ['学习英文', '学习react']
  }
  this.handleInputChange = this.handleInputChange.bind(this)
  this.handleBtnClick = this.handleBtnClick.bind(this)
  this.handleDeleteItm = this.handleDeleteItm.bind(this)
}

// 在组件即将被挂载到页面的时候执行
componentWillMount() {
  console.log('componentWillMount')
}

render() {
  console.log('parent render')
  return (
    <div>......</div>
  )
}

// 组件被挂载到页面之后自动执行
componentDidMount() {
  console.log('componentDidMount')
}

// 当组件被更新之前,他会自动执行
shouldComponentUpdate() {
  console.log('shouldComponentUpdate')
  return true
}

// 组件被更新之前,它会自动执行,但是它在 shouldComponentUpdate 之后执行
// 如果 shouldComponentUpdate 返回true它才执行
// 如果返回 false ,这个函数就不会执行了
componentWillUpdate() {
  console.log('componentWillUpdate')
}

componentDidUpdate() {
  console.log('componentDidUpdate')
}

在子组件中

render() {
  console.log('child render')
  const { content } =  this.props
  return (
    <div onClick={this.handleClick}>
      {content}
    </div>
  )
}

handleClick() {
  const { deleteItem, index } = this.props
  deleteItem(index)
}

// 一个组件从父组件接受参数
// (只要父组件的render函数被重新执行了,子组件的这个生命周期函数就会被执行)
// 如果这个组件第一次存在于父组件中,不会执行
// 如果这个组件之前已经存在于父组件中,才会执行
componentWillReceiveProps() {
  console.log('child componentWillReceiveProps')
}

// 但这个组件即将被剔除时执行
componentWillUnmount() {
  console.log('child componentWillUnmount')
}

Mount 是指组件被挂载执行的过程,Updation 是指组件被更新执行的过程,什么情况发生更新呢?要么是 state 被更新,要么是 props 被更新,也就是数据发生变化的时候,页面会更新

需要注意的是,所有的生命周期函数都可以不存在,但是有一个生命周期函数必须得有,就是 render 函数

它的底层为什么会有这样的设定呢?原因就是组件是继承自 Component 这个组件的,React Component 这个组件里边默认内置了其他所有的生命周期函数,唯独没有内置 render 函数,所以对组件来说,render 是必须自己定义的,不然就会报错

React 生命周期函数的使用场景

上面讲了 React 的生命周期函数,有一个比较容易忽视的钩子函数 shouldComponentUpdate ,他的使用过程是怎样的呢?

我们先来做个测试,看下子组件的渲染过程,先把所有的生命周期函数都删除掉,只在子组件的 render 函数中留下 console.log('child render')

然后我们在 input 框中输入内容,在控制台可以看到这样的结果

image

也就是父组件 render 函数重新执行的时候,子组件的 render 函数也会跟着执行

这样的逻辑是没有问题的,但是它会带来性能上的损耗

父组件上的内容发生变化了,其实子组件的内容是没必要重新渲染的,而这样的机制会导致子组件要做很多无谓的渲染

那应该怎样做性能优化呢?

很简单,这个时候我们就可以利用生命周期函数 shouldComponentUpdate 来做性能优化了

shouldComponentUpdate 这个函数的意思是,当数据或者内容发生变化的时候,会先询问一下,组件是否要被真正的更新

shouldComponentUpdate(nextProps, nextState) {
  if(nextProps.content !== this.props.content) {
    return true
  } else {
    return false
  }
}

shouldComponentUpdate 一般会接收两个参数,一个是nextProps,另一个是 nextState

当一个组件要被更新的时候,props 要被更新成什么样呢?
nextProps 指的是接下来 props 要被变化成什么样,nextState 指的是接下来 state 要被变化成什么样

我们的组件 props 接收的 content ,如果 content 发生变化,这个组件才需要重新渲染,没有发生变化时,不需要发生渲染

这样就通过 shouldComponentUpdate 这个生命周期函数提升了组件的性能,可以避免一个组件做无谓的 render 操作

render 函数重新执行,就意味着 React 底层要生成一份 Virtual DOM ,和之前的 Virtual DOM 做比对,虽然 Virtual DOM 的比对比 Actual DOM 的比对要快的多,但是,如果能省略这个比对过程当然能节约更多的性能

性能优化

当然,React 当中有很多关于性能优化的点

  • 首先是 this.handleClick.bind(this) 这样的方法,如果要改变作用域的话,我们把作用域的修改放在 constructor 里边,这样可以保证整个程序里边这个函数的作用域绑定只会执行一次,而且可以避免组件的一些无谓渲染,所以,这样写代码,react 组件的性能会有所提升

  • 其次, react 的底层 setState 内置了性能提升的机制,是一个异步的函数,可以把多次数据的改变结合成一次来做,这样可以降低 Virtual DOM 的比对频率

  • 再者 react 的底层使用了 Virtual DOM 的概念,还有同层比对,还有 key 的概念,来提升 Virtual DOM 比对的速率,从而提升 react 的性能

  • 最后,也就是借助 shouldComponentUpdate 这个方法,可以提高 react 组件的性能,因为我们可以避免无谓的组件的 render 函数的运行

监听数据对象

由于之前用过一段时间的 Vue,在转到 React 开发的时候,可以明显的发现 React 并没有 Vue 可以 watch 数据对象的方法

React 是怎么检测数据对象的变化呢?

React 默认不是双向绑定的,它不监听数据对象,而是通过手动调用 setState() 方法来触发了 Virtual DOM 的更新,再用 diff 算法来进行 Virtual DOM 比较前后两个状态的不同,看看是哪个 DOM 节点更新了,然后针对性的更改变化了的 DOM 结构实现数据更新,渲染 Actual DOM

我们单纯的使用 React,状态发生变化,会触发组件生命周期中的如下方法:

componentWillUpdate(object nextProps, object nextState) 

componentDidUpdate(object prevProps, object prevState) 

但如果结合 Redux 使用,一般状态变化是由 Dispatch 引起的,我们可以在 Dispatch 的回调中执行相应的操作

image

函数式编程

react 把需要不断重复构建的 UI 抽象成了组件,它充分利用很多函数式的方法减少了冗余代码

可以说,函数式编程是 React 的精髓

那到底什么是函数式编程呢?

函数式编程或称函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象

比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

也就是说,函数式编程和命令式编程最大的区别是:

函数式编程关心数据的映射,而命令式编程关心解决问题的步骤

而且维护方便,面向测试的开发流程

一个高阶函数,它可以接收函数可以当参数,也可以当返回值,这就是函数式编程

像柯里化、装饰器模式、高阶组件,都是相通的,一个道理

举个简单的 🌰

function first () {
  console.log('zhangshan')
}
function second() {
  console.log('lisi')
}

现在想在每条 console 语句前后各加一条 console 语句,如果在每个函数都加上 console 语句,会产生不必要的耦合,所以高阶函数就派上了用场

function FuncWrapper(func) {
  return function () {
    console.log('before')
    func()
    console.log('after')
  }
}
var first = FuncWrapper(first)
var second = FuncWrapper(second)

我们写了一个函数 FuncWrapper,该函数接一个函数作为参数,将参数函数装饰了一层,返回出去,减少了代码耦合

在设计模式中称这种模式为装饰器或装饰者模式

React 中,高阶组件 HOC 就相当于这么一个 FuncWrapper,传入一个组件,返回被包装或者被处理的另一个组件

高阶组件

a higher-order component is a function that takes a component and returns a new component.

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件

高阶组件就是一个没有副作用的纯函数

本质上是一个类工厂,先举个简单的 🌰

组件一:

import React from 'react'

export default class First extends React.Component {
  constructor (props) {
    super(props)
    this.changeHandle = this.changeHandle.bind(this)
  }
  changeHandle (value) {
    console.log(value)
  }
  render () {
    return (
      <div>
        <h2>zhangsan</h2>
        <input type="text" onchange={value => this.changeHandle(value)}/>
      </div>
    )
  }
}

组件二:

import React from 'react'

export default class Second extends React.Component {
  constructor (props) {
    super(props)
    this.changeHandle = this.changeHandle.bind(this)
  }
  changeHandle (value) {
    console.log(value)
  }
  render () {
    return (
      <div>
        <h2>lisi</h2>
        <input type="text" onchange={value => this.changeHandle(value)}/>
      </div>
    )
  }
}

有两个不相同的组件,但是有部分功能重合,比如 h2 标题的内容,changeHandle 函数,这样也就造成了代码的冗余

理解了高阶函数,再解决这类问题就不难了吧?接下来我们加入高阶组件解决这个问题

高阶组件:

import React, { Fragment } from 'react'

// 定义装饰器的外层函数
function HocUITest(name) {
  // 返回一个装饰器函数
  return function CompWrapper (Component) {
    return class WarpComponent extends React.Component {
      constructor (props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
      }

      handleChange (value) {
        console.log(value)
      }

      render () {
        return (
          <Fragment>
            <h2>{name}</h2>
            <Component handleChange={this.handleChange} {...this.props}></Component>
          </Fragment>
        )
      }
    }
  }
}
export default HocUITest

在高阶组件返回包装好的组件的时,我们将高阶组件的 props 展开并传入包装好的组件中,这是确保给高阶组件的 props 也能给到被包装的组件上

简化接下来的两个组件

组件一:

import React from 'react'
import HocUITest from './Test'

@HocUITest('zhangsan')
class First extends React.Component {
  constructor (props) {
    super(props)
    console.log(props)
  }
  render () {
    return (
      <div>
        <input type="text" onChange={value => this.props.handleChange(value)}/>
      </div>
    )
  }
}

// First = HocUITest('zhangsan')(First)

export default First

组件二:

import React from 'react'
import HocUITest from './Test'

@HocUITest('lisi')
export default class Second extends React.Component {
  constructor (props) {
    super(props)
    console.log(props)
  }
  render () {
    return (
      <div>
        <input type="text" onChange={value => this.props.handleChange(value)}/>
      </div>
    )
  }
}

高阶组件的用途很多,比如代码复用,逻辑抽象,抽离底层代码,渲染劫持,更改 state、更改 props 等等

包括我们经常用到的 react-reduxconnect 函数

reduxstateaction 创建函数,通过 props 注入给了 Component

你在目标组件 Component 里面可以直接用 this.props 去调用 redux stateaction 创建函数了

ConnectedComment = connect(mapStateToProps, mapDispatchToProps)(Component)

相当于

// connect是一个返回函数的函数(就是个高阶函数)
const enhance = connect(mapStateToProps, mapDispatchToProps)
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(Component)

antdForm 组件也是一样的

const WrappedNormalLoginForm = Form.create()(NormalLoginForm)

上述高阶组件中我们用了 ES6 装饰器语法,@HocUITest('lisi') 就是一个装饰器,它修改了类的行为

也就是说,装饰器是一个对类进行处理的函数

需要注意的是,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时

这意味着,装饰器能在编译阶段运行代码

也就是说,装饰器本质就是编译时执行的函数

为了传递更多的参数,上面的装饰器函数外面又封装了一层函数

比如,我们实际开发时,ReactRedux 库结合使用时,常常需要写成下面这样

class MyComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

有了装饰器,就可以改写上面的代码

@connect(mapStateToProps, mapDispatchToProps)
export default class MyComponent extends React.Component {}

接下来我们主要说一下两种功能的 react 高阶组件:属性代理、反向继承

属性代理

高阶组件将它收到的 props 传递给被包装的组件,所叫属性代理

主要用来处理以下问题

  • 更改 props
  • 抽取 state
  • 通过 refs 获取组件实例
  • 将组件与其他原生 DOM 包装到一起

反向继承

为什么叫反向继承,是高阶组件继承被包装组件,按照我们想的被包装组件继承高阶组件

反向代理主要用来做渲染劫持

所谓的渲染劫持,就是最后组件所渲染出来的东西或者我们叫 React Element 完全由高阶组件来决定,通过我们可以对任意一个 React Elementprops 进行操作;我们也可以操作 React ElementChild

用过 React-Redux 的人可能会有印象,使用 connect 可以将 reactredux 关联起来,这里的 connect 就是一个高阶组件

ref 的使用

refreference 的简写,它是一个引用,在 React ,可以使用 ref 操作 DOM

<input
  id='insertArea'
  className='input'
  value={this.state.inputValue} 
  onChange={this.handleInputChange}
  ref={(input) => {this.input = input}}
/>

React 16 的新语法中,ref 应该等于一个函数(箭头函数)

ref={(input) => {this.input = input}} ,构造了一个 ref 引用,这个引用叫 this.input ,它指向 input 对应的 DOM 节点。所以 this.input 指向的就是 input 框的 DOM

所以下面的方法

handleInputChange(e) {
  const value = e.target.value
  this.setState(() => ({
      inputValue: value
  }))
}

可以改为

handleInputChange() {
    const value = this.input.value
    this.setState(() => ({
        inputValue: value
    }))
  }

应该尽量保持少操作 DOM ,setState 是异步函数,操作 DOM 时必须写到回调中

比如

<Fragment>
  <div>
    <label htmlFor='insertArea'>输入内容</label>
    <input
      id='insertArea'
      className='input'
      value={this.state.inputValue} 
      onChange={this.handleInputChange}
      ref={(input) => {this.input = input}}
    />
    <button onClick={this.handleBtnClick}>提交</button>
  </div>
  <ul ref={(ul) => {this.ul = ul}}>
    {this.getTodoItem()}
  </ul>
</Fragment>
 handleBtnClick() {
  this.setState((prevState) =>({
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  }), () => {
    console.log(this.ul.querySelectorAll('div').length)
  })
}

ref 是帮助我们在 React 中直接获取 DOM 元素的时候使用的,一般情况下尽量避免使用 ref ,但是有的时候一些极其复杂的业务,比如动画的时候,不可避免的还是要用到 DOM 标签,怎么用呢,就用 ref 来获取 DOM 标签

注意,refsetState 合用的时候,DOM 的获取并不及时,原因是 setState 是异步的,如果希望页面更新之后再去获取 DOM ,需要把获取 DOM 的语法放在 setState 的第二个参数里边,它是一个回调函数

补充知识

开发环境搭建

快速搭建 React 的开发环境有两种方法

  1. 通过 CDN 引入 .js 文件来使用 React

  2. 使用 create-react-app 脚手架工具来编码

脚手架是前端开发过程中的一个辅助工具,自动构建一个大型项目的开发流程和目录,允许我们以一定方式实现 js 文件的相互引用,更方便的对项目进行管理

在脚手架的代码并不能直接运行,需要脚手架进行编译,编译出来的代码才可以被浏览器识别运行,一般会使用 webpackgulp 这样的工具

工程目录简介

image

  • yarn.lock - 项目依赖的安装包
  • package.json - node 的包文件,包含项目的介绍、项目依赖的包、指令供调用,让项目变成node的包
  • public favicon.ico - 项目左上角图标,index.html 模板
  • src index.js - 整个程序运行的入口文件

注意事项

state 不允许做任何改变,可以先拷贝 state 中的值

const list = [...this.state.list]

需要注意的是

  1. 在 JSX 语法中, {{}} 是表示 JS 表达式里的 JS 对象

  2. 转义的情况下,比如输入:<h1>hello</h1> 会显示 <h1>hello</h1>

    <li 
      key={index} 
      onClick={this.handleDeleteItm.bind(this,index)}
    >
      {item}
    </li>
    

不转义的情况下,比如输入:<h1>hello</h1> 会显示 hello

```jsx
<li 
  key={index} 
  onClick={this.handleDeleteItm.bind(this,index)}
  dangerouslySetInnerHTML={{__html: item}}
>
</li>
```
  1. React 中使用表单时,label 元素的 for 标签要替换成 htmlFor

    <label htmlFor='insertArea'>输入内容</label>
    <input
      id='insertArea'
      className='input'
      value={this.state.inputValue} 
      onChange={this.handleInputChange.bind(this)}
    />
    

使用 Charles 实现本地数据 mock

在前端开发代码的时候实际上和后端是分离的,也就需要在本地进行接口数据的模拟,这个时候就需要使用 Charles 进行接口数据的模拟

具体操作我在博客大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux里边已经讲到了,有兴趣的小伙伴可以去看看

我们先 npm install axios --save 安装 axios

然后在程序中引入 axios: import axios from 'axios'

componentDidMount() {
  axios.get('/list.json')
    .then((res) => { 
      console.log('data',res.data) 
      this.setState(() => ({      
          list: [...res.data]
      }))
    })
    .catch(() => console.log('error'))
}

image

Charles 这个工具的原理是什么?

它可以抓到浏览器向外发送的请求,然后对一些请求做一些处理,比如说,抓取到请求的是http://localhost:3000/list.json

他有一个规则是,只要你请求的下面这个地址

image

就会把 Local path 这个本地文件的内容返回给你

所以 Charles 其实就是一个中间的代理服务器,可以抓取到浏览器的请求,如果有些接口是需要模拟的话,就可以使用 CharlesMap Local 这个功能去模拟数据

当然,用脚手架的话,我们也可以在 public 文件夹中 mock 请求

结尾

这篇文章主要从编程思想入手剖析 React,包括如何快速构建组件和应用,让你快速了解 React 的编程原理

当然,对于构建大型应用,我们还需要结合 Reduxreact-routeraxios,大家有兴趣的话,都可以瞧瞧 ^_^